using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml;

namespace Pinky.Xml
{
    public class XmlSerializer
    {
        private interface IGeneralizedTypeConverter
        {
            object FromString(string data);
            string ToString(object data);
        }

        private class TypeConverterGeneralizingWrapper<T> : IGeneralizedTypeConverter
        {
            private readonly ITypeConverter<T> _concrete;

            public TypeConverterGeneralizingWrapper(ITypeConverter<T> concrete)
            {
                _concrete = concrete;
            }

            public object FromString(string data)
            {
                return _concrete.FromString(data);
            }

            public string ToString(object data)
            {
                return _concrete.ToString((T)data);
            }
        }

        private static readonly object[] EmptyObjectArray = new object[0];

        private readonly Dictionary<Type, IGeneralizedTypeConverter> _primitiveConverters =
            new Dictionary<Type, IGeneralizedTypeConverter>();

        public XmlSerializer()
        {
            var converters = AppDomain.CurrentDomain.GetAssemblies().SelectMany(
                a =>
                a.GetTypes().Where(
                    t =>
                        !t.IsAbstract
                        && t.IsClass
                        && t.GetConstructors().Length > 0
                        && t.GetInterfaces().Any(i =>
                            i.IsGenericType &&
                            i.GetGenericTypeDefinition() == typeof(ITypeConverter<>))));

            foreach (var converter in converters)
            {
                var tg = converter
                    .GetInterfaces().First(i =>
                        i.IsGenericType &&
                        i.GetGenericTypeDefinition() == typeof(ITypeConverter<>))
                    .GetGenericArguments()[0];

                var wrapp = Activator.CreateInstance(
                    typeof(TypeConverterGeneralizingWrapper<>).MakeGenericType(tg),
                    CreateInstance(converter));

                _primitiveConverters[tg] = (IGeneralizedTypeConverter)wrapp;
            }
        }

        public void Write(XmlWriter xmlWriter, object simpleEntity, string rootName)
        {

            xmlWriter.WriteStartElement(rootName);
            WriteNode(xmlWriter, simpleEntity, simpleEntity.GetType());
            xmlWriter.WriteEndElement();
        }

        private void WriteTree(XmlWriter xmlWriter, object simpleEntity, Type objType)
        {
            var properties = FindProperties(objType);

            foreach (var propertyInfo in properties)
            {
                xmlWriter.WriteStartElement(propertyInfo.Name);
                object value = propertyInfo.GetValue(simpleEntity, EmptyObjectArray);

                if (!Equals(value, GetDefault(propertyInfo.PropertyType)))
                {
                    Type valueType = value.GetType();
                    if (valueType != propertyInfo.PropertyType)
                    {
                        xmlWriter.WriteAttributeString("type", GetTypeAlias(valueType));
                    }

                    WriteNode(xmlWriter, value, valueType);
                }

                xmlWriter.WriteEndElement();
            }
        }

        private void WriteNode(XmlWriter xmlWriter, object value, Type valueType)
        {
            if (_primitiveConverters.ContainsKey(valueType))
            {
                xmlWriter.WriteString(_primitiveConverters[valueType].ToString(value));
            }
            else
            {
                WriteTree(xmlWriter, value, valueType);
            }
        }

        private string GetTypeAlias(Type valueType)
        {
            return valueType.AssemblyQualifiedName;
        }

        private static IEnumerable<PropertyInfo> FindProperties(Type objType)
        {
            return objType.GetProperties()
                .Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0)
                .OrderBy(p => p.Name);
        }

        private object GetDefault(Type type)
        {
            return type.IsValueType ? CreateInstance(type) : null;
        }

        public T Read<T>(XmlReader rdr)
        {
            var elementType = typeof(T);

            while (rdr.Read())
            {
                if (rdr.IsStartElement()) // ignore 1st element
                {
                    return (T)ReadTree(rdr, elementType);
                }
            }

            throw new InvalidOperationException("Xml contained no elements!");
        }

        private object ReadTree(XmlReader rdr, Type elementType)
        {
            var properties = FindProperties(elementType);
            var propertyMap = properties.ToDictionary(p => p.Name);
            var obj = CreateInstance(elementType);

            while (rdr.Read())
            {
                if (rdr.IsStartElement())
                {
                    if (!propertyMap.ContainsKey(rdr.Name)) continue;

                    var prop = propertyMap[rdr.Name];

                    Type valueType;

                    if (rdr.MoveToAttribute("type") && rdr.ReadAttributeValue())
                    {
                        valueType = GetTypeFromAlias(rdr.Value);
                        if (!prop.PropertyType.IsAssignableFrom(valueType))
                        {
                            throw new SerializerException(
                                "Mapped alias '" + rdr.Value + "' of type " +
                                valueType.AssemblyQualifiedName +
                                " is not convertible to property type " + prop.PropertyType.AssemblyQualifiedName);
                        }
                    }
                    else
                    {
                        valueType = prop.PropertyType;
                    }

                    object value = _primitiveConverters.ContainsKey(valueType) ?
                        _primitiveConverters[valueType].FromString(rdr.ReadString())
                        : ReadTree(rdr, valueType);

                    prop.SetValue(obj, value, EmptyObjectArray);
                }
            }
            return obj;
        }

        private object CreateInstance(Type elementType)
        {
            return Activator.CreateInstance(elementType);
        }

        private Type GetTypeFromAlias(string typeName)
        {
            return Type.GetType(typeName);
        }
    }
}